── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr 1.1.4 ✔ readr 2.1.5
✔ forcats 1.0.0 ✔ stringr 1.5.1
✔ lubridate 1.9.4 ✔ tibble 3.2.1
✔ purrr 1.0.4 ✔ tidyr 1.3.1
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag() masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(wordcloud2)
datos <-read_csv("data/idealista-sale-properties-spain.csv")
Rows: 29625 Columns: 15
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (11): property_url, title, location, price, price_m2, features, descrip...
dbl (3): property_id, energy_consumption, energy_emissions
date (1): scraped_date
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
dim(datos)
[1] 29625 15
n_distinct(datos)
[1] 29625
1 Descripción del dataset
¿Por qué es importante y qué pregunta/problema pretende responder? Resume brevemente las variables que lo forman y su tamaño.
Asumiremos que somos una startup con la intención de ofrecer un servicio de búsqueda de propiedades inmobiliarias en España. Para ello, hemos recopilado un conjunto de datos de propiedades en venta a través del portal Idealista, que contiene información sobre las características de las propiedades, su ubicación y otros detalles relevantes.
Nuestra intención es doble: por un lado, pretendemos poder ofrecer asesoramiento personalizado a nuestros clientes sobre el valor real de los inmuebles que se encuentran ofertados, por lo que sería interesante disponer de un modelo que nos permita estimar el precio de un inmueble en función de su ubicación y características.
Por el otro lado, puesto que nos encontramos en una fase inicial del negocio, nos gustaría conocer si existen distintos perfiles de propiedades que nos permitan segmentar el mercado. De esta manera, podremos contratar a asesoradores especializados en distintos tipos de propiedad para ofrecer un trato personalizado.
Para resolver estas cuestiones, vamos a utilizar el conjunto de datos que describimos en la práctica anterior, el cual consiste en un conjunto de datos con 29625 entradas y 15 columnas. Cada entrada corresponde a una propiedad inmobiliaria en venta en la página web de Idealista, y las columnas contienen información sobre las características de la propiedad, su ubicación, el precio, entre otros.
head(datos)
str(datos)
spc_tbl_ [29,625 × 15] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
$ property_id : num [1:29625] 1.04e+08 1.07e+08 1.07e+08 1.07e+08 9.73e+07 ...
$ property_url : chr [1:29625] "https://www.idealista.com/inmueble/103799139/" "https://www.idealista.com/inmueble/106861711/" "https://www.idealista.com/inmueble/107370179/" "https://www.idealista.com/inmueble/107491937/" ...
$ title : chr [1:29625] "Casa o chalet independiente en venta en avenida Touroperador Cosmos, 3" "Casa o chalet independiente en venta en Iger" "Casa o chalet independiente en venta en calle Laranxeira" "Piso en venta en Aingeru Zaindaria Bidea, 60" ...
$ location : chr [1:29625] "Maspalomas-Meloneras, San Bartolomé de Tirajana" "Hondarribia" "Priegue, Nigran" "Añorga, Donostia-San Sebastián" ...
$ price : chr [1:29625] "1.425.000" "3.900.000" "1.750.000" "650.000" ...
$ price_m2 : chr [1:29625] "3.589 €/m²" "6.240 €/m²" "2.747 €/m²" "5.000 €/m²" ...
$ features : chr [1:29625] "397 m²,6 hab." "625 m²,7 hab.,Garaje incluido" "637 m²,5 hab.,Garaje incluido" "130 m²,4 hab.,Planta 2ª exterior sin ascensor,Garaje incluido" ...
$ description : chr [1:29625] "¡UNA JOYA EN EL CORAZÓN DE CAMPO INTERNACIONAL! Si buscas tranquilidad, amplitud y una ubicación envidiable, es"| __truncated__ "¡Descubre el paraíso en Hondarribi con esta espectacular chalet independiente! Situada en la mejor zona de la c"| __truncated__ "Descubre el hogar de tus sueños en la exclusiva zona de Los Abetos, en Nigrán. Este impresionante chalet, que s"| __truncated__ "Ángel de la Guarda- Vivienda ubicada en un entorno tranquilo y a 5 minutos en coche del barrio de Ibaeta y de s"| __truncated__ ...
$ energy_consumption: num [1:29625] 108 NA NA 286 NA ...
$ energy_emissions : num [1:29625] 24 NA NA 61 NA NA 69.4 NA NA 1 ...
$ address : chr [1:29625] "Avenida Touroperador Cosmos, 3,Distrito Maspalomas-Meloneras,San Bartolomé de Tirajana,Gran Canaria, Las Palmas" "Iger,Hondarribia,Bajo Bidasoa, Guipúzcoa" "Calle Laranxeira,Distrito Priegue,Nigran,Área de Vigo, Pontevedra" "Aingeru Zaindaria Bidea, 60,Distrito Añorga,Donostia-San Sebastián,Donostialdea, Guipúzcoa" ...
$ agent_name : chr [1:29625] "Gran Canaria Sun Properties" "Iñigo Zubiarrain" "T L" "Areizaga Inmobiliaria" ...
$ agent_ref : chr [1:29625] "8667" "106861711" "2927-TE" "37249" ...
$ last_updated : chr [1:29625] "31 de marzo" "23 de enero" "26 de marzo" "19 de marzo" ...
$ scraped_date : Date[1:29625], format: "2025-04-01" "2025-04-01" ...
- attr(*, "spec")=
.. cols(
.. property_id = col_double(),
.. property_url = col_character(),
.. title = col_character(),
.. location = col_character(),
.. price = col_character(),
.. price_m2 = col_character(),
.. features = col_character(),
.. description = col_character(),
.. energy_consumption = col_double(),
.. energy_emissions = col_double(),
.. address = col_character(),
.. agent_name = col_character(),
.. agent_ref = col_character(),
.. last_updated = col_character(),
.. scraped_date = col_date(format = "")
.. )
- attr(*, "problems")=<externalptr>
features_words <- datos |>unnest_tokens(word, features) |>filter(!word %in%c(0:9,stopwords("es"))) |>count(word, sort =TRUE)head(features_words)
description_words <- datos |>unnest_tokens(word, description) |>filter(!word %in%c(0:9,stopwords("es"))) |>count(word, sort =TRUE)wordcloud2(description_words)
2 Integración y selección de los datos de interés a analizar
Puede ser el resultado de adicionar diferentes datasets o una subselección útil de los datos originales, en base al objetivo que se quiera conseguir. Si se decide trabajar con una selección de los datos, es muy importante que esta esté debidamente justificada. Además, se recomienda mostrar un resumen de los datos que permita ver a simple vista las diferentes variables y sus rangos de valores.
Como hemos visto, en el conjunto de datos encontramos una serie de campos de texto libre. Aunque este tipo de campos permiten una gran flexibilidad a la hora de almacenar información, añaden cierta complejidad al análisis. A continuación, extraeremos una serie de campos estructurados a partir de la información que hemos obtenido del análisis de texto anterior:
¿Los datos contienen ceros, elementos vacíos u otros valores numéricos que indiquen la pérdida de datos? Gestiona cada uno de estos casos utilizando el método de imputación que consideres más adecuado.
Identifica y gestiona adecuadamente el tipo de dato de cada atributo (p.ej. conversión de variables categóricas en factor).
Identifica y gestiona los valores extremos.
Justifica la necesidad de otros métodos de limpieza para este dataset en particular y, de ser necesario, aplícalos.
Nota: se ha decidido contestar los apartados anteriores en un orden diferente al que aparecen en el enunciado con el objetivo de facilitar la tarea y su comprensión. En este caso, se ha decidido primero transformar los datos a un formato más adecuado para su análisis y después aplicar las técnicas de limpieza e imputación de datos.
Tal y como veíamos en primer lugar, el dataset contiene una serie de variables almacenadas en formato de cadena y que deberían transformarse en variables numéricas o factores, previo a analizar sus correspondientes rangos y niveles, así como valores extremos:
Vamos en primer lugar a hacer un análisis preliminar de las características de los datos, atendiendo especialmente al rango de datos en las variables numéricas:
summary(datos_conv |>select(where(is.numeric)))
price price_m2 energy_consumption energy_emissions
Min. : 1000 Min. : 1 Min. : 0.01 Min. : 0.01
1st Qu.:109000 1st Qu.: 1375 1st Qu.: 100.00 1st Qu.: 24.00
Median :230000 Median : 2344 Median : 160.00 Median : 38.00
Mean :286039 Mean : 3100 Mean : 198.44 Mean : 63.15
3rd Qu.:399999 3rd Qu.: 4011 3rd Qu.: 236.00 3rd Qu.: 56.20
Max. :999999 Max. :37225 Max. :9999.00 Max. :9999.00
NA's :21825 NA's :21962
superficie habitaciones
Min. : 0.0 Min. : 1.00
1st Qu.: 93.0 1st Qu.: 3.00
Median :138.0 Median : 3.00
Mean :193.1 Mean : 3.54
3rd Qu.:240.0 3rd Qu.: 4.00
Max. :998.0 Max. :58.00
NA's :259
Llama la atención el valor máximo de las variables energy_consumption y energy_emissions, en particular su valor máximo (9999) se aleja mucho de la media y la mediana correspondientes. Vamos a comprobar la existencia de valores extremos:
Warning: Removed 44046 rows containing non-finite outside the scale range
(`stat_bin()`).
De los histogramas anteriores parece desprenderse que la mayoría de las variables numéricas muestran una distribución con una marcada desviación hacia la derecha. Llama la atención la existencia de entradas con valor cero para las variables price (0 entradas), price_m2 (0 entradas) y superficie (41 entradas).
Queda claro que la mayoría de los valores perdidos pertenecen a las variables energy_consumption y energy_emissions, seguidos de las variables piso, distrito y ciudad.
datos_imp <-missRanger( datos_conv, data_only =TRUE,formula = superficie + energy_consumption + energy_emissions + piso + ascensor + garaje + exterior + habitaciones + calle + barrio + distrito + ciudad + provincia ~ . - title - description - features - property_id - property_url- address - agent_ref - last_updated - scraped_date,num.trees =50, max.depth =5, pmm.k =5, seed =123,)
Una vez hemos realizado la imputación de las variables predictoras, comprobemos ahora la distribución de las variable a predecir:
hist(datos_imp$price)
Encontramos que la distribución es muy sesgada hacia la derecha, por lo que vamos a aplicar una transformación de la variable para reducir su asimetría. En este caso, vamos a aplicar una transformación de la familia Box-Cox, que es adecuada para variables con distribuciones sesgadas. Tal y como veremos posteriormente, la transformación necesaria en nuestro caso es la transformación Box-Cox con \(\lambda=\dfrac{1}{4}\):
hist(datos_imp$price ^ (1/4))
Encontamos que la variable precio incluso tras ser transformada, parece seguir una distribución bimodal, lo cual probablemente afectará el rendimiento de nuestro modelo. Veamos si podemos superar este escollo utilizando la variable price_m2 en su lugar:
hist(datos_imp$price_m2^(1/4))
Observamos que la distribución de la variable de price_m2 es mucho más simétrica, por lo que la utilizaremos como variable a predecir en lugar de la variable price. Si el precio del inmueble es lo que nos interesa, podremos posteriormente multiplicar la variable price_m2 predicha por la superficie de cada inmueble.
4 Análisis de los datos
import numpy as npimport pandas as pdimport sklearn as skimport seaborn as snsimport matplotlib.pyplot as plt
from rpy2 import robjects
/nix/store/jff219hnrn02rzdqrjjccrrq7hx52vxg-python3-3.12.10-env/lib/python3.12/site-packages/rpy2/rinterface_lib/embedded.py:276: UserWarning: R was initialized outside of rpy2 (R_NilValue != NULL). Trying to use it nevertheless.
warnings.warn(msg)
R was initialized outside of rpy2 (R_NilValue != NULL). Trying to use it nevertheless.
from rpy2.robjects import r, pandas2ri, Environmentpandas2ri.activate()datos = pandas2ri.rpy2py(r['datos_imp'])datos = datos[~datos.price_m2.isna()]datos.head()
Aplica un modelo supervisado y uno no supervisado a los datos y comenta los resultados obtenidos.
4.1 Modelo supervisado
En primer lugar, desarrollaremos un modelo de regresión con el objetivo de predecir el precio de mercado en función de las características del inmueble. Dada la naturaleza tabular de nuestros datos, probaremos dos modelos diferentes que suelen presentar un buen desempeño en este tipo de conjuntos de datos: un modelo de regresión basado en árboles de decisión y otro basado en gradient boosting.
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Como aproximación inicial, vamos a aplicar un modelo de reducción dimensional de tipo manifold, especializado en permitir la visualización bidimensional de datos de alta dimensión. En este caso, utilizaremos el algoritmo t-SNE, que es especialmente útil para visualizar datos complejos y con relaciones no lineales.
Vaya, esperábamos encontrar un pequeño número de clusters con características comunes, pero lo que observamos aquí son numerosas agrupaciones más o menos dispersas.
Como hipótesis inicial, se nos ocurre que las agrupaciones que observamos puedan estar determinadas por la ubicación geográfica de los inmuebles. Al fin y al cabo, tiene sentido que las viviendas situadas en un mismo área presenten características estructurales y de precio similares. Vamos a comprobarlo:
Confirmamos que los datos parecen corroborar nuestra hipótesis: las agrupaciones reflejan la ubicación de los inmuebles. Como conclusión accionable entonces, consideramos una prioridad disponer de vendedores especializados en áreas geográficas concretas, ya que es probable que las características de las propiedades y sus precios varíen significativamente entre diferentes ciudades y barrios.
Aplica una prueba por contraste de hipótesis. Ten en cuenta que algunas de estas pruebas requieren verificar previamente la normalidad y homocedasticidad de los datos.
Durante una de nuestras numerosas y tediosas reuniones internas, uno de nuestros asesores inmobiliarios mencionó que, en su experiencia, las propiedades con ascensor tienden a tener un precio por metro cuadrado más alto que aquellas sin ascensor. Otro de nuestros asesores, sin embargo, argumentó que esta diferencia no es significativa y que el precio por metro cuadrado depende más de la ubicación y otras características de la propiedad.
Puesto que nos caracterizamos por ser una organización orientada al dato, pretendemos resolver esta cuestión de forma objetiva, utilizando una prueba estadística que nos permita determinar si existe una diferencia significativa en el precio por metro cuadrado entre las propiedades con y sin ascensor.
from statsmodels.stats.diagnostic import kstest_normalres = kstest_normal(datos.price_m2)print(f"Normality test for property prices: {res[1]:.3f}")
Normality test for property prices: 0.001
Nota: el test de normalidad de Kolmogorov-Smirnov indica que los precios por metro cuadrado no siguen una distribución normal, con un p-valor muy bajo (p < 0.001). No obstante, dado el elevado número de registros presentes en el conjunto de datos, este test puede ser muy sensible a pequeñas desviaciones de la normalidad. Por ello, es necesario matizar que nos apoyamos fundamentalmente en el análisis Q-Q.
Es evidente que los precios no parecen seguir una distribución normal, por lo que a priori no podemos aplicar una prueba paramétrica como la t de Student. Se decide por tanto aplicar una prueba no paramétrica, como la prueba U de Mann-Whitney, que es adecuada para comparar dos muestras independientes cuando no se cumplen los supuestos de normalidad de los datos:
import statsmodels.stats.nonparametric as nonplift_properties = datos[datos.ascensor ==2]nolift_properties = datos[datos.ascensor ==1]res = nonp.rank_compare_2indep(lift_properties.price_m2, nolift_properties.price_m2)print(f"Mann-Whitney's U rank-sum test: W={res.statistic:.3f}, p-value={res.pvalue:.3f}")
Mann-Whitney's U rank-sum test: W=69.942, p-value=0.000
Comprobamos que el resultado de la prueba U de Mann-Whitney es significativo, lo que indica que existe una diferencia en el precio por metro cuadrado entre las propiedades con y sin ascensor. Podríamos aquí concluir nuestro análisis al respecto. Sin embargo, vamos a comprobar si podemos aplicar una normalización de los datos que nos permita aplicar una prueba paramétrica, como la t de Student, para comparar las medias de los dos grupos.
Dada la marcada desviación de los datos hacia la derecha, vamos a aplicar una serie de transformaciones de la familia Box-Cox, que son adecuadas para transformar variables con distribuciones sesgadas. Probaremos inicialmente con la transformación cuadrática, que es una transformación comúnmente utilizada para reducir la asimetría de los datos (\(\lambda=\dfrac{1}{2}\)):
hist(sqrt(datos_conv$price_m2))
Vemos que no resulta suficiente. Tras varias pruebas de ensayo y error, encontramos que la transformación con \(\lambda=\dfrac{1}{4}\) es la que muestra una distribución de los datos más similar a la normalidad:
Una vez aplicada la transformación, comprobamos que los datos parecen seguir una distribución normal, por lo que podemos aplicar una prueba paramétrica como la t de Student. Antes de ello, vamos a comprobar si las varianzas de los dos grupos son homogéneas, lo cual es un supuesto necesario para aplicar la prueba t:
from scipy.stats import leveneres = levene( lift_properties.price_m2 ** (1/4), nolift_properties.price_m2 ** (1/4), center='mean')print(f"Levene's test for homogeneity of variances: F={res.statistic:.3f}, p-value={res.pvalue:.3f}")
Levene's test for homogeneity of variances: F=271.025, p-value=0.000
Dado que la prueba de Levene indica que las varianzas de los dos grupos son heterogéneas (p < 0.01), aplicaremos la versión de Welch de la prueba t, que no asume homogeneidad de varianzas:
Como se puede comprobar, la conclusión derivada de ambos análisis es la misma: las propiedades con ascensor tienen en general un precio por metro cuadrado más alto que aquellas sin ascensor, y esta diferencia es estadísticamente significativa. Por lo tanto, podemos concluir que la afirmación de nuestro asesor inmobiliario es correcta.
5 Representación de los resultados a partir de tablas y gráficas
Este apartado se puede responder a lo largo de la práctica, sin necesidad de concentrar todas las representaciones en este apartado. Se debe representar tanto el contenido del dataset para observar las proporciones y distribuciones de las diferentes variables una vez aplicada la etapa de limpieza, como los resultados obtenidos tras la etapa de análisis.
6 Resolución del problema.
A partir de los resultados obtenidos, ¿cuáles son las conclusiones? ¿Los resultados permiten responder al problema?